查看原文
其他

一个例子教你学会DialogFragment —模仿国际微博评论框

2017-08-23 showzeng 终端研发部

前言介绍

用 DialogFragment 模仿国际微博评论框。

showzeng的博客地址:

 http://showzeng.itscoder.com/android/2017/08/11/the-imitation-of-the-international-weibo-comment-box.html

正文

前言:在写家园内部办公系统 US APP 话题页面的时候,因为自己绞尽脑汁也想不出什么好的设计图,脑海中唯一印象深刻的就是国际微博的 UI 了,索性就模仿着来吧,于是有了本文。

啥也不说了,先上图


分析

1.从图中,可以看出,微博详情页底下的一个评论条其实并不做输入框使用,而是充当一个 Button 的作用,唤起真正的输入框。

2.真正的输入框,从效果来看,像是 Dialog,这里选用了自定义布局的 DialogFragment。

3.输入框 Dismiss 或者 Cancel 时,若评论并未提交,则将其显示到底部的评论条中,单行显示,溢出省略。

4.再次进入输入框时,若评论条内容不为空,需将文本内容填充到输入框中。

5.评论框中无字符输入时,提交按钮为灰色且不可触发,反之为正常的提交按钮。

分析完了那就开工吧

  1. 底部评论条  

这个就比较简单了,一个 LinearLayout 包裹住头像控件和文本控件且置于屏幕底部。这里想要提出的一点:

Tip 1: 直接选用 TextView 而不是 EditText

在一开始的时候,我的第一直觉是认为那是一个 EditText,所以直接用的这个控件,设置为不可编辑,一切都看似那么的合乎情理。直到调试的时候,我发现在首次进入页面的时候,第一次点击评论条,并不能触发点击事件,需要第二次点击时才有效,而后就不会再出现问题。我试图去找过是什么原因,但未果,也不排除是否是机型或者什么其他原因,但倾向于是控件的事件分发哪里被截断了。总之,如果你能找到原因,还请告知,让我学习学习,感激不尽。

之后转念一想,反正 TextView 也是可以设置 Hint 的,那为啥不直接用 TextView 呢,简单省事。

下面是布局代码:

<!-- activity_main.xml --> <?xml version="1.0" encoding="utf-8"?> <RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="@color/background_my_material_grey"    android:orientation="vertical">    ...... </RelativeLayout>

  2. DialogFragment 评论框  

评论框布局也是比较简单的,一个无背景的输入框,一个提交按钮,以及左侧几个附加的功能按钮,比如选图,和 @ 功能,这个就直接上布局文件了:

<!-- dialog_fragment_comment_layout.xml --> <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:orientation="vertical"              android:layout_width="match_parent"              android:layout_height="130dp"              android:layout_gravity="bottom"              android:background="@color/background_my_material_light">   ...... </RelativeLayout>

  3. DialogFragment 逻辑代码  

关于 DialogFragment 用法,网上搜一下就有很多了。首先必须要实现的是 onCreateView() 或者是 onCreateDialog() 方法,这里比较核心的应该就是布局的位置了,因为 DialogFragment 默认布局是常见的 Dialog 样式,这里需要手动设置:

// CommentDialogFragment.java public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{    private Dialog mDialog;    private EditText commentEditText;    private ImageView photoButton;    private ImageView atButton;    private ImageView sendButton;    @Override    public Dialog onCreateDialog(Bundle savedInstanceState) {        // 自定义 style BottomDialog        mDialog = new Dialog(getActivity(), R.style.BottomDialog);        mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);        mDialog.setContentView(R.layout.dialog_fragment_comment_layout);        // 外部点击设置为可以取消        mDialog.setCanceledOnTouchOutside(true);        Window window = mDialog.getWindow();        WindowManager.LayoutParams layoutParams = window.getAttributes();        // 布局属性位于整个窗口底部        layoutParams.gravity = Gravity.BOTTOM;        // 布局属性宽度填充满整个窗口宽度,默认是有 margin 值的        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;        window.setAttributes(layoutParams);        commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);        photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);        atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);        sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);        photoButton.setOnClickListener(this);        atButton.setOnClickListener(this);        sendButton.setOnClickListener(this);        return mDialog;    }    @Override    public void onClick(View v) {        switch (v.getId()) {            case R.id.image_btn_photo:                Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();                break;            case R.id.image_btn_at:                Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();                break;            case R.id.image_btn_comment_send:                Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();                dismiss();                break;            default:                break;        }    } }

自定义的 BottomDialog style:

<!-- styles.xml --> <resources>    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">        ...    </style>    <style name="BottomDialog" parent="@style/AppTheme">        <item name="android:layout_width">match_parent</item>        <item name="android:layout_height">wrap_content</item>        <item name="android:windowIsFloating">true</item>    </style> </resources>

  4. MainActivity 逻辑代码  

此时只需要为评论条添加监听器,响应 DialogFragment 的呼出即可:

// MainActivity.java public class MainActivity extends AppCompatActivity implements View.OnClickListener{    private TextView commentFakeButton;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        commentFakeButton = (TextView) findViewById(R.id.tv_comment_fake_button);        commentFakeButton.setOnClickListener(this);    }    @Override    public void onClick(View v) {        switch (v.getId()) {            case R.id.tv_comment_fake_button:                CommentDialogFragment commentDialogFragment = new CommentDialogFragment();                commentDialogFragment.show(getFragmentManager(), "CommentDialogFragment");                break;            default:                break;        }    } }

此时,运行程序,大体上是没问题了,接下来就需要实现它的一些小 Feature,一个是评论文本内容填充,一个是评论框文本输入监听,控制评论提交按钮的状态。

  5. 评论文本内容填充  

这里第一反应可能是普通 Activity 与 Fragment 间的通信,之后在网上看文章时,学习到回调这种用法,也是第一次比较深入地去理解回调这门艺术,不得不说,回调真是个好东西。接下来我们就来看看怎么用回调来实现这一个 Feature。

首先需要定义一个接口:

// DialogFragmentDataCallback.java public interface DialogFragmentDataCallback {    String getCommentText();    void setCommentText(String commentTextTemp); }

代码很简单,这里比较好的理解方式是,一个类实现这个接口,那么就必须重写这两个方法,而 get 方法,可以看作是提供给外部类调用,用来获取这个类的一些数据,而 set 方法,可看作是外部类用来回调操作此类的一个暴露的方法。那接下来就让 MainActivity 实现这个接口:

// MainActivity.java public class MainActivity extends AppCompatActivity implements View.OnClickListener, DialogFragmentDataCallback{    private TextView commentFakeButton;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        commentFakeButton = (TextView) findViewById(R.id.tv_comment_fake_button);        commentFakeButton.setOnClickListener(this);    }    @Override    public void onClick(View v) {        switch (v.getId()) {            case R.id.tv_comment_fake_button:                CommentDialogFragment commentDialogFragment = new CommentDialogFragment();                commentDialogFragment.show(getFragmentManager(), "CommentDialogFragment");                break;            default:                break;        }    }    @Override    public String getCommentText() {        // 提供给外部类获取本类中数据的方法,这里就是用于获取评论条的文本,将其返回给外部类就可以        return commentFakeButton.getText().toString();    }    @Override    public void setCommentText(String commentTextTemp) {        // 提供给外部类用来回调设置评论条文本内容的方法        commentFakeButton.setText(commentTextTemp);    } }

接着在 CommentDialogFragment 中实现文本的填充:

// CommentDialogFragment.java public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{    private Dialog mDialog;    private EditText commentEditText;    private ImageView photoButton;    private ImageView atButton;    private ImageView sendButton;    @Override    public Dialog onCreateDialog(Bundle savedInstanceState) {        mDialog = new Dialog(getActivity(), R.style.BottomDialog);        mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);        mDialog.setContentView(R.layout.dialog_fragment_comment_layout);        mDialog.setCanceledOnTouchOutside(true);        Window window = mDialog.getWindow();        WindowManager.LayoutParams layoutParams = window.getAttributes();        layoutParams.gravity = Gravity.BOTTOM;        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;        window.setAttributes(layoutParams);        commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);        photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);        atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);        sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);        // 填充文本方法        fillEditText();        photoButton.setOnClickListener(this);        atButton.setOnClickListener(this);        sendButton.setOnClickListener(this);        return mDialog;    }    private void fillEditText() {        // 获取 MainActivity 实例并转型为 DialogFragmentDataCallback 接口        dataCallback = (DialogFragmentDataCallback) getActivity();        // 回调获取评论条文本内容并填充至输入框中        commentEditText.setText(dataCallback.getCommentText());    }    @Override    public void onClick(View v) {        switch (v.getId()) {            case R.id.image_btn_photo:                Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();                break;            case R.id.image_btn_at:                Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();                break;            case R.id.image_btn_comment_send:                Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();                // 提交成功后 ( 根据具体需求一般为网络请求发送评论 ) 清空评论框, 重写 onDismiss,使评论框文本内容填充回评论条,这里提交后就设为空                commentEditText.setText("");                dismiss();                break;            default:                break;        }    }    @Override    public void onDismiss(DialogInterface dialog) {        // 重写 onDismiss,将评论框文本填充回评论条        dataCallback.setCommentText(commentEditText.getText().toString());        super.onDismiss(dialog);    }    @Override    public void onCancel(DialogInterface dialog) {        // 重写 onCancel,将评论框文本填充回评论条,一般是点击外部及返回键所触发        dataCallback.setCommentText(commentEditText.getText().toString());        super.onCancel(dialog);    } }

  6. 评论框文本输入监听,控制评论提交按钮的状态  

这个算是本文比较有趣的一部分,也是自己在实现这个项目效果中碰到坑最多的地方了,让我细细说来。首先是 EditText 的输入监听,使用的是 TextWatcher,当输入的文本字符大于 0 时,将 sendButton 禁用,设置颜色为灰色。

Tip 2: 在刚进入输入框,未执行动作前,TextWatcher 是无法监听此时状态的

因为刚开始进入输入框时,在还未执行输入动作之前,TextWatcher 无法监听此时的状态,所有方法都得不到调用。所以,在填充文本时,就应该要做一个判断,因为 ImageView 充当 Button 默认设置的是可点击的,所以在文本为空时,需要将发送按钮禁用。

Tip 3: ImageView 充当 Button,clickable 属性设置无效,应使用 enable 属性

在一开始时,我是试图用 clickable 属性来控制这个 ImageView 假 Button 的点击事件,但却发现这个属性完全失效不起作用。不是说 ImageView 默认是不可点击的吗?那我先在 xml 里设置 clickable 属性为 false 也是没用的,后来经大佬提点,才知道是监听器的原因。为 ImageView 设置 OnClickListener 时,会自动将其属性 clickable 设置为 true,这点请看源码:

// View.java /** * Register a callback to be invoked when this view is clicked. If this view is not * clickable, it becomes clickable. * * @param l The callback that will run * * @see #setClickable(boolean) */ public void setOnClickListener(@Nullable OnClickListener l) {    if (!isClickable()) {        setClickable(true);    }    getListenerInfo().mOnClickListener = l; }

既然如此,那就在代码中设置其 enable 属性,将其完全禁用:

// CommentDialogFragment.java public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{    private Dialog mDialog;    private EditText commentEditText;    private ImageView photoButton;    private ImageView atButton;    private ImageView sendButton;    @Override    public Dialog onCreateDialog(Bundle savedInstanceState) {        mDialog = new Dialog(getActivity(), R.style.BottomDialog);        mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);        mDialog.setContentView(R.layout.dialog_fragment_comment_layout);        mDialog.setCanceledOnTouchOutside(true);        Window window = mDialog.getWindow();        WindowManager.LayoutParams layoutParams = window.getAttributes();        layoutParams.gravity = Gravity.BOTTOM;        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;        window.setAttributes(layoutParams);        commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);        photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);        atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);        sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);        fillEditText();        // 为 EditText 设置监听器        commentEditText.addTextChangedListener(mTextWatcher);        photoButton.setOnClickListener(this);        atButton.setOnClickListener(this);        sendButton.setOnClickListener(this);        return mDialog;    }    private void fillEditText() {        dataCallback = (DialogFragmentDataCallback) getActivity();        commentEditText.setText(dataCallback.getCommentText());        // 若评论条文本内容为空,禁用按钮        if (dataCallback.getCommentText().length() == 0) {            sendButton.setEnabled(false);            sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));        }    }    // TextWatcher,主要重写 afterTextChanged 方法    private TextWatcher mTextWatcher = new TextWatcher() {        private CharSequence temp;        @Override        public void beforeTextChanged(CharSequence s, int start, int count, int after) {            temp = s;        }        @Override        public void onTextChanged(CharSequence s, int start, int before, int count) {        }        @Override        public void afterTextChanged(Editable s) {            // 输入动作触发后,将进行监听,若文本被清空,禁用按钮,反之,则恢复正常            if (temp.length() > 0) {                sendButton.setEnabled(true);                sendButton.setClickable(true);                sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.colorAccent));            } else {                sendButton.setEnabled(false);                sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));            }        }    };    @Override    public void onClick(View v) {        switch (v.getId()) {            case R.id.image_btn_photo:                Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();                break;            case R.id.image_btn_at:                Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();                break;            case R.id.image_btn_comment_send:                Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();                commentEditText.setText("");                dismiss();                break;            default:                break;        }    }    @Override    public void onDismiss(DialogInterface dialog) {        dataCallback.setCommentText(commentEditText.getText().toString());        super.onDismiss(dialog);    }    @Override    public void onCancel(DialogInterface dialog) {        dataCallback.setCommentText(commentEditText.getText().toString());        super.onCancel(dialog);    } }

  7. 在弹出评论框时呼出软键盘  

关于软键盘的呼出问题,也是一个比较头疼的问题,自己也碰到了比较多坑,网上的解决方案比较多,也比较多没有用 :) 。文末有推荐几篇比较优秀的文章可供参考。

Tip 4: 在给 EditText 获取焦点 (requestFocus() 方法) 之前,需设置 setFocusable 和 setFocusableInTouchMode 为 true

第一个要提出的点是:在填充评论条文本内容到评论框后,需要调用 EditText 的 setSelection 方法设置光标位置,不然魅族默认没有光标,也不会有软键盘弹出,我不知道是不是只有我的手机会这样,反正用另一部华为手机就没有这个问题。( 这里有点奇怪的是,为什么国际微博评论框弹出时,光标是置于文本头的,感觉这里很不人性化 )

第二个是软键盘的弹出,之前就是怎么都弹不出来,之后在一篇文章中看到说设置延时是很关键的一步,我试过之后,发现确实如此,可是所有文章都未提到为什么一定要设置延时,唯一说到一点的是说软键盘的加载需要一点时间,当然,我仍然不知道这里为何,这个理由完全不是需要延时的原因啊。延时时间是我手动调试过去的,但发现偶尔还是弹不出来,这里实在是不解,而且不同手机又怎么保证延时时间是正确的?如果你知道这个问题的答案,还请告知,让我学习学习,感激不尽。下面是 CommentDialogFragment 完整的代码:

// CommentDialogFragment.java public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{    private Dialog mDialog;    private EditText commentEditText;    private ImageView photoButton;    private ImageView atButton;    private ImageView sendButton;    @Override    public Dialog onCreateDialog(Bundle savedInstanceState) {        mDialog = new Dialog(getActivity(), R.style.BottomDialog);        mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);        mDialog.setContentView(R.layout.dialog_fragment_comment_layout);        mDialog.setCanceledOnTouchOutside(true);        Window window = mDialog.getWindow();        WindowManager.LayoutParams layoutParams = window.getAttributes();        layoutParams.gravity = Gravity.BOTTOM;        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;        window.setAttributes(layoutParams);        commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);        photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);        atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);        sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);        fillEditText();        // 呼出软键盘方法        setSoftKeyboard();        commentEditText.addTextChangedListener(mTextWatcher);        photoButton.setOnClickListener(this);        atButton.setOnClickListener(this);        sendButton.setOnClickListener(this);        return mDialog;    }    private void fillEditText() {        dataCallback = (DialogFragmentDataCallback) getActivity();        commentEditText.setText(dataCallback.getCommentText());        // Write this line for meizu 特别的爱,给特别的你        commentEditText.setSelection(dataCallback.getCommentText().length());        if (dataCallback.getCommentText().length() == 0) {            sendButton.setEnabled(false);            sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));        }    }    private void setSoftKeyboard() {        // 为 EditText 获取焦点        commentEditText.setFocusable(true);        commentEditText.setFocusableInTouchMode(true);        commentEditText.requestFocus();        // TODO: 17-8-11 为何这里要延时才能弹出软键盘, 延时时长又如何判断? 目前是手动调试        Timer timer = new Timer();        timer.schedule(new TimerTask() {            @Override            public void run() {                inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);                inputMethodManager.toggleSoftInput(0, InputMethodManager.SHOW_FORCED);            }        }, 110);    }    private TextWatcher mTextWatcher = new TextWatcher() {        private CharSequence temp;        @Override        public void beforeTextChanged(CharSequence s, int start, int count, int after) {            temp = s;        }        @Override        public void onTextChanged(CharSequence s, int start, int before, int count) {        }        @Override        public void afterTextChanged(Editable s) {            if (temp.length() > 0) {                sendButton.setEnabled(true);                sendButton.setClickable(true);                sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.colorAccent));            } else {                sendButton.setEnabled(false);                sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));            }        }    };    @Override    public void onClick(View v) {        switch (v.getId()) {            case R.id.image_btn_photo:                Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();                break;            case R.id.image_btn_at:                Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();                break;            case R.id.image_btn_comment_send:                Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();                commentEditText.setText("");                dismiss();                break;            default:                break;        }    }    @Override    public void onDismiss(DialogInterface dialog) {        dataCallback.setCommentText(commentEditText.getText().toString());        super.onDismiss(dialog);    }    @Override    public void onCancel(DialogInterface dialog) {        dataCallback.setCommentText(commentEditText.getText().toString());        super.onCancel(dialog);    } }

写在最后

本文项目完整代码已提交到 GitHub 上,如有需要,请自取。PureComment 项目链接

本文到这里差不多就结束了,感谢阅读。关于字数限制和输入字符数量提示,这个在 TextWatcher 里监听就可以实现了,因为自己项目中评论并没有字数限制,所以这里没有写这个部分,不过相信讲到这里,自己去实现也是很简单的啦!可参考下面给出的参考文档。

最后,如果你有更好的实现方法或者什么建议,欢迎联系,一起讨论交流。Thank you and have a nice day!

扩展阅读,参考文档

Android 官方推荐 : DialogFragment 创建对话框

DialogFragment 实现底部弹窗

DialogFragment 生命周期简介

知乎:回调函数(callback)是什么?

onTextChanged 参数解释及实现 EditText 字数监听

Android 软键盘隐藏寻找最优解


demo项目地址::

https://github.com/showzeng/PureComment

终端研发部提倡 没有做不到的,只有想不到的

在这里获得的不仅仅是技术!


让心,在阳光下学会舞蹈

让灵魂,在痛苦中学会微笑

—终端研发部—



如果你觉得此文对您有所帮助,欢迎入群 QQ交流群 :232203809   

微信公众号:终端研发部


            

这里学到不仅仅是技术


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存